/**
 *
 */
package steven.gui.console;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;

/**
 * @author steven.lam.t.f
 *
 */
public abstract class Console extends JComponent implements Scrollable, KeyListener{
	private static final long serialVersionUID = 2005089279717535867L;
	private static final int PADDING = 5;
	private final List<ConsoleLine> lines = new ArrayList<>();
	private final ConsoleController controller;
	private Font font;
	private int fontWidth;
	private int fontHeight;
	private boolean fontChanged;
	private int maxLineWidth;
	private final ConsoleInputLine inputLine = new ConsoleInputLine();

	public Console(final ConsoleController controller){
		this.controller = controller;
		this.fontWidth = 1;
		this.fontHeight = 1;
		this.maxLineWidth = 0;
		this.lines.add(new ConsoleLine());
		this.setFont(new Font("MS Gothic", Font.PLAIN, 20));
		super.setFocusTraversalKeysEnabled(false);
		super.addKeyListener(this);
	}
	@Override
	protected void paintComponent(final Graphics g){
		if(this.fontChanged){
			final FontMetrics fontMetrics = g.getFontMetrics(this.font);
			this.fontWidth = fontMetrics.charWidth('a');
			this.fontHeight = fontMetrics.getHeight();
			this.fontChanged = false;
			super.revalidate();
			this.controller.repaint();
		}
		g.setFont(this.font);
		final Rectangle bounds = g.getClipBounds();
		int currentY = this.fontHeight;
		int lineIndex = 0;
		final int endY;
		if(bounds == null){
			endY = Integer.MAX_VALUE;
		}else{
			while(currentY < bounds.y){
				currentY += this.fontHeight;
				lineIndex++;
			}
			if(lineIndex > 0){
				currentY -= this.fontHeight;
				lineIndex--;
			}
			endY = bounds.y + bounds.height + this.fontHeight;
		}
		for(; lineIndex < this.lines.size() && currentY < endY; lineIndex++){
			final ConsoleLine line = this.lines.get(lineIndex);
			g.drawChars(line.chars, 0, line.length, Console.PADDING, currentY);
			currentY += this.fontHeight;
		}
		if(lineIndex >= this.lines.size()){
			currentY -= this.fontHeight;
			final Color color = g.getColor();
			g.setColor(Color.RED);
			g.drawRect(Console.PADDING + this.getCursorOffsetWidth() * this.fontWidth, currentY, 2, -this.fontHeight);
			g.setColor(color);
			g.drawChars(this.inputLine.chars, 0, this.inputLine.length, Console.PADDING + this.getLastLineOutputWidth() * this.fontWidth, currentY);
		}
	}
	@Override
	public Dimension getPreferredScrollableViewportSize(){
		// this function is never called
		return null;
	}
	@Override
	public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation, final int direction){
		switch(orientation){
			case SwingConstants.VERTICAL:
				return this.fontHeight;
			case SwingConstants.HORIZONTAL:
				return this.fontWidth;
			default:
				throw new IllegalArgumentException("Invalid orientation: " + orientation);
		}
	}
	@Override
	public int getScrollableBlockIncrement(final Rectangle visibleRect, final int orientation, final int direction){
		switch(orientation){
			case SwingConstants.VERTICAL:
				return visibleRect.height;
			case SwingConstants.HORIZONTAL:
				return visibleRect.width;
			default:
				throw new IllegalArgumentException("Invalid orientation: " + orientation);
		}
	}
	@Override
	public boolean getScrollableTracksViewportWidth(){
		// enable horizontal scrolling
		return false;
	}
	@Override
	public boolean getScrollableTracksViewportHeight(){
		// enable vertical scrolling
		return false;
	}
	@Override
	public void setFont(final Font font){
		this.font = font;
		this.fontChanged = true;
		super.setFont(font);
	}
	@Override
	public Dimension getPreferredSize(){
		return new Dimension(Console.PADDING + Math.max(this.maxLineWidth, this.getLastLineWidth()) * this.fontWidth + this.fontWidth + Console.PADDING, (this.lines.size() + 1) * this.fontHeight + Console.PADDING);
	}
	@Override
	public void keyTyped(final KeyEvent event){
		if(event.getModifiersEx() == 0){
			final char c = event.getKeyChar();
			if(c >= 32 && c != 127){
				this.inputLine.insert(c);
				this.scrollToCursorAndRepaint();
			}
		}
		System.out.println("typed " + (int)event.getKeyChar());
	}
	@Override
	public void keyPressed(final KeyEvent event){
		final int keyCode = event.getKeyCode();
		if(event.getModifiersEx() == 0){
			if(keyCode == KeyEvent.VK_BACK_SPACE){
				this.inputLine.deletePreviousCharacter();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_DELETE){
				this.inputLine.deleteNextCharacter();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_HOME){
				this.inputLine.moveCursorToBeginning();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_END){
				this.inputLine.moveCursorToEnd();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_LEFT){
				this.inputLine.moveCursorBackwards();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_RIGHT){
				this.inputLine.moveCursorForwards();
				this.scrollToCursorAndRepaint();
			}else if(keyCode == KeyEvent.VK_ENTER){
				this.processInput(new String(this.inputLine.chars, 0, this.inputLine.length));
				this.scrollToCursorAndRepaint();
			}
		}else if(event.isControlDown() && keyCode == KeyEvent.VK_V){
			System.out.println("hello");
			final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
			try{
				this.inputLine.insert((String)clipboard.getData(DataFlavor.stringFlavor));
			}catch(final Exception e){
				// do nothing
			}
			this.scrollToCursorAndRepaint();
		}
		System.out.println("pressed " + event.getKeyCode() + " " + event.getKeyLocation() + " " + event.getModifiers() + " " + event.getModifiersEx());
	}
	@Override
	public void keyReleased(final KeyEvent event){
		System.out.println("released " + event.getKeyCode() + " " + event.getKeyLocation() + " " + event.getModifiers() + " " + event.getModifiersEx());
	}
	public void clear(){
		this.maxLineWidth = 0;
		this.lines.clear();
		this.lines.add(new ConsoleLine());
		this.scrollToCursorAndRepaint();
	}
	public void appendText(final String s){
		final ConsoleLine lastLine = this.lines.get(this.lines.size() - 1);
		final String[] parts = s.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1);
		lastLine.append(parts[0]);
		this.maxLineWidth = Math.max(this.maxLineWidth, lastLine.lineWidth);
		for(int i = 1; i < parts.length; i++){
			final ConsoleLine line = new ConsoleLine(parts[i]);
			this.lines.add(line);
			this.maxLineWidth = Math.max(this.maxLineWidth, line.lineWidth);
		}
		this.scrollToCursorAndRepaint();
	}
	public void appendNewLine(){
		this.lines.add(new ConsoleLine());
		this.scrollToCursorAndRepaint();
	}
	private void scrollToCursorAndRepaint(){
		super.revalidate();
		if(this.inputLine.getCursorPosition() == 0){
			super.scrollRectToVisible(new Rectangle(0, this.lines.size() * this.fontHeight, this.fontWidth, this.fontHeight));
		}
		super.scrollRectToVisible(new Rectangle(Console.PADDING + this.getCursorOffsetWidth() * this.fontWidth, this.lines.size() * this.fontHeight, this.fontWidth, this.fontHeight));
		this.controller.repaint();
	}
	private int getLastLineOutputWidth(){
		return this.lines.get(this.lines.size() - 1).lineWidth;
	}
	private int getLastLineWidth(){
		return this.getLastLineOutputWidth() + this.inputLine.lineWidth;
	}
	private int getCursorOffsetWidth(){
		return this.getLastLineOutputWidth() + this.inputLine.getCursorOffsetWidth();
	}
	protected abstract void processInput(String input);
	protected abstract void onCreate();
	protected final ConsoleController getController(){
		return this.controller;
	}
	protected final ConsoleInputLine getInputLine(){
		return this.inputLine;
	}
}
